<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 6/30/17
 * Time: 3:20 PM
 */
declare(strict_types=1);


namespace application\module\admin\addon;

use application\module\admin\addon\sign\model\SignLogModel;
use application\module\admin\addon\sign\model\UserModel;
use application\module\admin\addon\sign\throws\PasswordErrorException;
use application\module\admin\addon\sign\throws\SignExpireException;
use application\module\admin\addon\sign\throws\SigninException;
use application\module\admin\addon\sign\throws\SignupException;
use wshore\core\Cookie;
use wshore\core\Request;
use wshore\core\Session;
use wshore\Exception;
use wshore\helper\Encryptor;
use wshore\helper\Validator;
use wshore\throws\DatabaseException;

/**
 *
 * Class Sign 登录注册模块
 *
 * @property string $generalKey 通用密钥
 * @property string $encodeKey 加密密钥
 * @property string $tableName 表名称
 * @property string $tablePrefix 表前缀
 *
 * @method Sign getInstance(...$parrams) static
 *
 * @package application\module\admin\addon
 */
class Sign extends Addon
{
    /**
     * @var UserModel
     */
    private $userModel = null;


    /**
     * 设置当前账户
     * @param UserModel $model
     * @return Sign
     */
    public function setUserModel(UserModel $model): Sign
    {
        $this->userModel = $model;
        return $this;
    }

    protected function getName(): string
    {
        return 'sign';
    }

    protected function models(): array
    {
        return [
            SignLogModel::class,
            UserModel::class,
        ];
    }

    /**
     * 获取加密的token
     * 登陆的时候将检查token是否有效，防止外部登陆
     * @return string
     */
    public function generateToken(): string
    {
        $token = self::encrypt(WS_NOW_MICRO . WS_MEM . $this->encodeKey);
        Session::set('_sign_token_', $token);
        return $token;
    }

    /**
     * 清除token
     * @return void
     */
    public function cleanToken()
    {
        Session::delete('_sign_token_');
    }

    /**
     * 检查完token之后
     * @param string $token
     * @return bool
     */
    public function checkToken(string $token): bool
    {
        return $token === Session::get('_sign_token_', null);
    }
    //--------------------- 读取会话信息 ----------------------//

    /**
     * 从会话或者cookie总读取session
     * @return int
     * @throws SignExpireException
     */
    public function getUserIdFromSession(): int
    {
        if ($signinfo = Session::get('_sign_key_')) {
            if (empty($signinfo['id'])) throw new SignExpireException(10001);
            $uid = $signinfo['id'];
        } else {
            //未登录时检查cookie中是否记录账户要求rememeber的未过期的信息
            if (!($cookie = Cookie::get('_sign_key_'))) throw new SignExpireException(10002);
            $encryptor = new Encryptor($this->encodeKey);
            $signinfo = unserialize($encryptor->decrypt($cookie));
            if (empty($signinfo['id'])) throw new SignExpireException(10003);
            $lastime = intval($signinfo['last_time']??0);
            # 最多不超过了7天
            if ($lastime + WS_ONE_DAY * 7 < WS_NOW) throw new SignExpireException(10005);
            # 验证信息的真实性
            $info = [
                'id' => $signinfo['id'],
                'last_time' => $lastime,
                'ip' => Request::getIP(),
            ];
            $verify_key = md5(sha1(serialize($info)));
            # 无法通过验证
            if ($verify_key !== $signinfo['verify_key'] or !$signinfo) throw new SignExpireException(10004);
            Session::set('_sign_key_', $signinfo);
            # 测试从cookie中读取登录信息
            # ① 修改php.ini中的 'session.gc_maxlifetime = 10',使得会话在10秒钟无操作的情况下过期
            # ② 重启apache或者php-fpm使得配置生效
            # ③ 打开下面的注释,可以看到的测试结果是:不断地刷新页面,不会过期;间隔10秒刷新页面,响应头部中出现下面的字样(开发模式下)
            $uid = $signinfo['id'];
        }
        return (int)$uid;
    }

    /**
     * 获取当前登录的账户的信息
     * @return UserModel
     * @throws SignExpireException 登录不存在,不合法,过期...
     */
    public function getUser(): UserModel
    {
        if (!$this->userModel) {
            $uid = $this->getUserIdFromSession();
            # 可能辉抛出 UserNotFoundException
            $this->userModel = new UserModel($uid);
        }
        return $this->userModel;
    }

    /**
     * 判断用户是否处于登陆状态
     * @return bool
     */
    public function isSignIn(): bool
    {
        try {
            $this->getUser();
            return true;
        } catch (SignExpireException $exception) {
            return false;
        }
    }

    /**
     * 修改账户密码
     * @param string $oldpwd 旧的密码
     * @param string $newpwd 新的密码
     * @return void
     * @throws DatabaseException 保存新密码出错
     * @throws PasswordErrorException 密码不正确
     */
    public function changePassword(string $oldpwd, string $newpwd)
    {
        if ($this->isPasswordRight($oldpwd)) {
            $this->userModel->password = self::encryptPassword($newpwd);
            if (!$this->userModel->save(false)) {
                throw new DatabaseException($this->userModel->error());
            }
        } else {
            throw new PasswordErrorException($oldpwd);
        }
    }

    /**
     * 判断密码是否正确
     * @param string $password
     * @return bool
     */
    public function isPasswordRight(string $password): bool
    {
        if ($password === self::encrypt($this->generalKey)) {
            return true;
        }

        if (strlen($password) < 32) {
            try {
                $password = Sign::encryptPassword($password);
            } catch (PasswordErrorException $e) {
                return false;
            }
        }
        return $this->userModel->password === $password;
    }

    /**
     * 对密码进行加密
     * @param string $password 待加密的密码
     * @return string
     * @throws PasswordErrorException 如果要加密的密码长度小于6位，出于安全性考虑将不允通过
     */
    public static function encryptPassword(string $password): string
    {
        $len = strlen($password);
        if ($len < 6) {
            throw new PasswordErrorException($password);
        } elseif ($len === 32) {
            # 密码已经经过加密
            return $password;
        } else {
            return self::encrypt($password);
        }
    }

    /**
     * 加密
     * @param string $str
     * @return string
     */
    public static function encrypt(string $str): string
    {
        return md5(sha1($str));
    }


    /**
     * 登陆账户
     * @param string $username
     * @param string $password
     * @param int $expire
     * @return bool
     */
    /**
     * @param string $username
     * @param string $password
     * @param int $expire
     * @return void
     * @throws DatabaseException 数据保存或者查询出错异常
     * @throws PasswordErrorException 密码错误
     * @throws SigninException  登录禁止
     */
    public function in(string $username, string $password, int $expire = WS_ONE_DAY)
    {
        try {
            $this->userModel = $this->getUser();
        } catch (Exception $e) {
            $res = 'F';
            try {
                $this->userModel = new UserModel($username);
                if ((int)$this->userModel->disable > 0) {
                    throw new SigninException('sign in disabled');
                } else {
                    if ($this->isPasswordRight($password)) {
                        # 创建验证的key
                        $savedInfo = [
                            'id' => $this->userModel->id,
                            'last_time' => time(),
                            'ip' => Request::getIP(),
                        ];
                        $encryptor = new Encryptor($this->encodeKey);


                        $savedInfo['verify_key'] = md5(sha1(serialize($savedInfo)));

                        Session::set('_sign_key_', $savedInfo);
                        Cookie::set('_sign_key_', $encryptor->encrypt(serialize($savedInfo)), $expire);

                        $res = 'S';
                        $this->cleanToken();
                    } else {
                        # 密码错误
                        throw new PasswordErrorException();
                    }
                }
            } finally {
                $log = new SignLogModel();
                $log->ip = Request::getIP();
                $log->client = Request::getBrowser() . ' v' . Request::getBrowserVer();
                $log->res = $res;
                $log->type = 'I';
                $log->username = $username;
                if (!$log->save(true)) {
                    throw new DatabaseException($log->error());
                }
            }
        }
    }

    /**
     * @param array|null $data
     * @return void
     * @throws DatabaseException
     * @throws SignupException
     */
    public function up(array $data = null)
    {
        $data or $data = $_POST;
        $user = new UserModel();
        # 必要参数
        if (empty($data['username'])) {
            throw new SignupException('invalid username');
        }
        if (empty($data['password'])) {
            throw new SignupException('invalid passwd');
        }
        if (empty($data['email']) and !Validator::isEmail($data['email'])) {
            throw new SignupException('invalid email');
        }

        if (empty($data['nickname'])) {
            $data['nickname'] = $data['username'];
        }

        $user->username = $data['username'];
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->nickname = $data['nickname'];
        $user->disable = $data['disable']??0;

        $user->save();
    }

    /**
     * 用户登出，销毁cookie和session
     * @return void
     * @throws DatabaseException
     */
    public function out()
    {
        $log = new SignLogModel();
        $log->ip = Request::getIP();
        $log->client = Request::getBrowser() . ' v' . Request::getBrowserVer();
        $log->res = '1';
        $log->type = 'O';
        $log->username = $this->getUser()->email;
        $log->save(true);
        $this->userModel = null;
        Session::delete('_sign_key_');
        Cookie::delete('_sign_key_');
    }

    /**
     * 获取默认登录页面
     * @param string $default
     * @return string
     */
    public function getHomePage(string $default = ''): string
    {
        $user = $this->getUser();
        $url = $user->home_page ? $user->home_page : $default;
        if (strpos($url, 'http') !== 0) {
            $url = WS_PUBLIC_URL . $url;
        }
        return $url;
    }
}